﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework;

namespace SceneManager
{
    public enum CameraControlType
    {
        FirstPerson,
        ThirdPerson
    }

    /// <summary>
    /// Represents a camera which can be used in a SceneManager
    /// </summary>
    public class Camera
    {
        private bool firstRun = true;

        /// <summary>
        /// Gets or sets the position of this camera
        /// </summary>
        public Vector3 Position { get; set; }
        /// <summary>
        /// Gets or sets the position where this camera is looking
        /// </summary>
        public Vector3 Target { get; set; }

        /// <summary>
        /// Gets or sets the distance of this camera from its target
        /// </summary>
        public float Distance { get; set; }
        /// <summary>
        /// Gets or sets the minimum distance between this camera and its target
        /// </summary>
        public float MinDistance { get; set; }
        /// <summary>
        /// Gets or sets the maximum distance between this camera and its target
        /// </summary>
        public float MaxDistance { get; set; }

        /// <summary>
        /// Gets or sets the control type of this camera (first or third person)
        /// </summary>
        public CameraControlType ControlType { get; set; }
        /// <summary>
        /// Gets or sets a factor to smooth camera movements (0 = no movements, 1 = no smoothing)
        /// </summary>
        public float SmoothFactor { get; set; }

        /// <summary>
        /// Gets the current view matrix for this camera
        /// </summary>
        public Matrix View { get; private set; }
        /// <summary>
        /// Gets the current projection matrix for this camera
        /// </summary>
        public Matrix Projection { get; private set; }

        /// <summary>
        /// Gets the frustum representing the view of this camera
        /// </summary>
        public BoundingFrustum Frustum { get; private set; }
        /// <summary>
        /// Gets the 4 far-corners of the frustum of this camera
        /// </summary>
        public Vector3[] FrustumCorners { get; private set; }

        /// <summary>
        /// Gets or sets the near plane in front of which this camera can't see
        /// </summary>
        public float NearPlane { get; set; }
        /// <summary>
        /// Gets or sets the far plane behind which this camera can't see
        /// </summary>
        public float FarPlane { get; set; }

        private Vector3 finalPosition;
        private Vector3 finalTarget;

        //public Vector3 Rotation { get; set; } // TODO: Use Rotation ?

        /// <summary>
        /// Gets or sets the current camera pitch (based on X-axis rotation)
        /// </summary>
        public float Pitch { get; set; }
        /// <summary>
        /// Gets or sets the minimum pitch for this camera
        /// </summary>
        public float MinPitch { get; set; }
        /// <summary>
        /// Gets or sets the maximum pitch for this camera
        /// </summary>
        public float MaxPitch { get; set; }

        /// <summary>
        /// Gets or sets the current camera yaw (based on Y-axis rotation)
        /// </summary>
        public float Yaw { get; set; }
        /// <summary>
        /// Gets or sets the minimum yaw for this camera
        /// </summary>
        public float MinYaw { get; set; }
        /// <summary>
        /// Gets or sets the maximum yam for this camera
        /// </summary>
        public float MaxYaw { get; set; }

        /// <summary>
        /// Gets or sets the current camera roll (based on Z-axis rotation)
        /// </summary>
        public float Roll { get; set; } // TODO: Min and Max

        private Game game;

        // TODO: Fix FirstPerson control type

        /// <summary>
        /// Initializes a new instance of the Camera class
        /// </summary>
        /// <param name="game">The game which will own the camera</param>
        public Camera(Game game)
        {
            // General parameters
            MinDistance = 1;
            MaxDistance = 5000;
            MinYaw = -MathHelper.Pi;
            MaxYaw = MathHelper.Pi;
            MinPitch = -MathHelper.PiOver2;
            MaxPitch = MathHelper.PiOver2;

            this.game = game;

            Position = Vector3.Backward;
            Target = Vector3.Zero;
            Distance = 1;

            SmoothFactor = 1;

            NearPlane = 0.1f;
            FarPlane = 10000;

            finalPosition = Position;
            finalTarget = Target;

            ControlType = CameraControlType.ThirdPerson;
        }

        /// <summary>
        /// Rotates this camera with the specified yaw, pitch and roll components
        /// </summary>
        /// <param name="yaw">The yaw component of the rotation</param>
        /// <param name="pitch">The pitch component of the rotation</param>
        /// <param name="roll">The roll component of the rotation</param>
        public void Rotate(float yaw, float pitch, float roll)
        {
            Yaw += yaw;
            Pitch -= pitch;
            Roll += roll;

            // Clamp values between -π and π
            while (Yaw > MathHelper.Pi)
                Yaw -= MathHelper.TwoPi;
            while (Yaw < -MathHelper.Pi)
                Yaw += MathHelper.TwoPi;

            while (Pitch > MathHelper.Pi)
                Pitch -= MathHelper.TwoPi;
            while (Pitch < -MathHelper.Pi)
                Pitch += MathHelper.TwoPi;

            while (Roll > MathHelper.Pi)
                Roll -= MathHelper.TwoPi;
            while (Roll < -MathHelper.Pi)
                Roll += MathHelper.TwoPi;

            Yaw = MathHelper.Clamp(Yaw, MinYaw + 0.001f, MaxYaw - 0.001f);
            Pitch = MathHelper.Clamp(Pitch, MinPitch + 0.001f, MaxPitch - 0.001f);

            if (ControlType == CameraControlType.ThirdPerson)
                finalPosition = finalTarget + Vector3.Transform(Vector3.Backward, Matrix.CreateFromYawPitchRoll(Yaw, -Pitch, 0)) * Distance;
            else if (ControlType == CameraControlType.FirstPerson)
                finalTarget = finalPosition + Vector3.Transform(Vector3.Forward, Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0)) * Distance;
        }
        /// <summary>
        /// Zooms the view by moving this camera closer of further of its target
        /// </summary>
        /// <param name="factor">The factor of the zoom (&lt;1 : closer, &gt;1 : further)</param>
        public void Zoom(float factor)
        {
            Distance *= factor;
            Distance = MathHelper.Clamp(Distance, MinDistance, MaxDistance);

            if (ControlType == CameraControlType.ThirdPerson)
                finalPosition = finalTarget + Vector3.Transform(Vector3.Backward, Matrix.CreateFromYawPitchRoll(Yaw, -Pitch, 0)) * Distance;
            else if (ControlType == CameraControlType.FirstPerson)
                finalTarget = finalPosition + Vector3.Transform(Vector3.Forward, Matrix.CreateFromYawPitchRoll(Yaw, -Pitch, 0)) * Distance;
        }
        /// <summary>
        /// Moves this cameras current position
        /// </summary>
        /// <param name="x">The x component of the translation</param>
        /// <param name="y">The y component of the translation</param>
        /// <param name="z">The z component of the translation</param>
        public void Move(float x, float y, float z)
        {
            if (ControlType == CameraControlType.ThirdPerson)
            {
                finalTarget += Vector3.Transform(new Vector3(x, y, z), Matrix.CreateFromYawPitchRoll(Yaw, 0, 0));
                finalPosition = finalTarget + Vector3.Transform(Vector3.Backward, Matrix.CreateFromYawPitchRoll(Yaw, -Pitch, 0)) * Distance;
            }
            else if (ControlType == CameraControlType.FirstPerson)
            {
                Position += new Vector3(x, y, z);
                finalPosition = Position;
            }
        }

        /// <summary>
        /// Updates this cameras position (used to smooth the movement)
        /// </summary>
        public void Update()
        {
            if (firstRun)
            {
                finalPosition = Position;
                finalTarget = Target;
            }
            firstRun = false;

            if (ControlType == CameraControlType.ThirdPerson)
            {
                Position = Vector3.Lerp(Position, finalPosition, SmoothFactor);
                finalTarget = Target;
            }
            else if (ControlType == CameraControlType.FirstPerson)
            {
                Target = Vector3.Lerp(Target, finalTarget, SmoothFactor);
                finalPosition = Position;
            }

            if (ControlType == CameraControlType.ThirdPerson)
                finalPosition = finalTarget + Vector3.Transform(Vector3.Backward, Matrix.CreateFromYawPitchRoll(Yaw, -Pitch, 0)) * Distance;
            else if (ControlType == CameraControlType.FirstPerson)
                finalTarget = finalPosition + Vector3.Transform(Vector3.Forward, Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0)) * Distance;

            // Compute the view and projection matrix
            View = Matrix.CreateLookAt(Position, Target, Vector3.Transform(Vector3.Up, Matrix.CreateFromYawPitchRoll(Yaw, -Pitch, Roll)));
            Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, game.GraphicsDevice.Viewport.AspectRatio, NearPlane, FarPlane);

            // Compute frustum and corners
            Frustum = new BoundingFrustum(View * Projection);

            Vector3[] frustumCorners = Frustum.GetCorners();
            for (int i = 0; i < 4; i++)
                frustumCorners[i] = frustumCorners[i + 4];
            Array.Resize(ref frustumCorners, 4);

            Vector3 corner = frustumCorners[2];
            frustumCorners[2] = frustumCorners[3];
            frustumCorners[3] = corner;

            FrustumCorners = frustumCorners;
        }
        /// <summary>
        /// Forces the update of the camera position and target with no smoothing
        /// </summary>
        public void UpdateFinal()
        {
            Rotate(0, 0, 0);

            if (ControlType == CameraControlType.ThirdPerson)
                Position = finalPosition = finalTarget + Vector3.Transform(Vector3.Backward, Matrix.CreateFromYawPitchRoll(Yaw, -Pitch, 0)) * Distance;
            else if (ControlType == CameraControlType.FirstPerson)
                Target = finalTarget = finalPosition + Vector3.Transform(Vector3.Forward, Matrix.CreateFromYawPitchRoll(Yaw, -Pitch, 0)) * Distance;
        }

        public Camera Reflect()
        {
            return null;
        }
    }
}
